package mutation

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/InVisionApp/conjungo"
	"github.com/gohobby/deepcopy"
	"github.com/wI2L/jsondiff"
	"gitverse.ru/synapse/kubelatte/pkg/api/common"
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/observability/logs"
	"gitverse.ru/synapse/kubelatte/pkg/storage"
	"gitverse.ru/synapse/kubelatte/pkg/util"
	"gitverse.ru/synapse/kubelatte/pkg/util/match"
	"gitverse.ru/synapse/kubelatte/pkg/util/templates"
	"html"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	lbs "k8s.io/apimachinery/pkg/labels"
	"reflect"
	"regexp"
	"sigs.k8s.io/yaml"
	"strings"
)

var MC *Controller

type Controller struct{}

func (r *Controller) ApplyMutation(ctx context.Context, arfields *common.ARFields, raw []byte) []jsondiff.Operation {
	var operations []jsondiff.Operation

	renders := r.getRenders(ctx, arfields)
	for _, templateRender := range renders {
		newObj := r.getNewObject(ctx, templateRender, raw)
		patch, err := jsondiff.Compare(arfields.Object, newObj)
		if err != nil {
			logs.Errorf("jsondiff.Compare error: %v", err)
			return nil
		}
		operations = append(operations, patch...)
	}
	return operations
}

func (r *Controller) getNewObject(ctx context.Context, templateRender util.RenderItem, raw []byte) map[string]interface{} {
	var newObj map[string]interface{}
	opts := conjungo.NewOptions()
	err := json.Unmarshal(raw, &newObj)
	if err != nil {
		logs.Errorf("ApplyMutation: unmarshal error: %v", err)
		return nil
	}

	var renderObj map[string]interface{}
	err = yaml.Unmarshal([]byte(html.UnescapeString(templateRender.Render)), &renderObj)
	if err != nil {
		logs.Errorf("location: yaml unmarshal error: %v", err)
		return nil
	}

	if templateRender.Action != "replace" {
		opts.SetKindMergeFunc(reflect.Slice,
			func(t, s reflect.Value, o *conjungo.Options) (reflect.Value, error) {
				aT, _ := t.Interface().([]interface{})
				aS, _ := s.Interface().([]interface{})
				aT = util.MergeSliceCommon(aT, aS, true)
				return reflect.ValueOf(aT), nil
			})
		opts.SetKindMergeFunc(reflect.Map,
			func(t, s reflect.Value, o *conjungo.Options) (reflect.Value, error) {
				aT, _ := t.Interface().(map[string]interface{})
				aS, _ := s.Interface().(map[string]interface{})
				aT = util.MergeStruct(aT, aS, true)
				return reflect.ValueOf(aT), nil
			})
	} else {
		opts.SetKindMergeFunc(reflect.Slice,
			func(t, s reflect.Value, o *conjungo.Options) (reflect.Value, error) {
				aS, _ := s.Interface().([]interface{})
				return reflect.ValueOf(aS), nil
			})
	}

	opts.Overwrite = templateRender.Action == "replace"
	renderObj = addMutatingLabel(renderObj)
	err = conjungo.Merge(&newObj, renderObj, opts)
	if err != nil {
		logs.Errorf("conjungo.Merge error: %v", err)
		return nil
	}

	return newObj
}

type objInfo struct {
	kind   string
	name   string
	annots map[string]string
	labels map[string]string
}

func getObjectInfoFromMap(obj map[string]interface{}) objInfo {
	objInf := objInfo{
		annots: map[string]string{},
		labels: map[string]string{},
	}

	kd, ok := obj["kind"]
	if ok {
		objInf.kind = kd.(string)
	}

	metadata, ok := obj["metadata"]
	if !ok {
		return objInf
	}

	tmpAn, ok := metadata.(map[string]interface{})["annotations"]
	if ok {
		for k, v := range tmpAn.(map[string]interface{}) {
			objInf.annots[k] = v.(string)
		}
	}

	tmpLb, ok := metadata.(map[string]interface{})["labels"]
	if ok {
		for k, v := range tmpLb.(map[string]interface{}) {
			objInf.labels[k] = v.(string)
		}
	}

	tmpName, ok := metadata.(map[string]interface{})["name"]
	if ok {
		objInf.name = tmpName.(string)
	}

	return objInf
}

func (r *Controller) getRenders(ctx context.Context, arfields *common.ARFields) []util.RenderItem {
	var renders []util.RenderItem
	triggers := storage.Storage.GetTriggers()

	for _, trigger := range triggers {
		renders = append(renders, r.getRendersForTrigger(ctx, trigger, *arfields)...)
	}
	return renders
}

func (r *Controller) getRendersForTrigger(ctx context.Context, trigger v1alpha1.Trigger, arfields common.ARFields) []util.RenderItem {
	var renders []util.RenderItem
	templs := storage.Storage.GetTemplates()
	obj := deepcopy.DeepCopy(arfields.Object).(map[string]interface{})
	object := getObjectInfoFromMap(obj)

	for _, config := range trigger.Spec.MutationConfigs {
		renders = r.getRendersForMutatingConfig(ctx, arfields, config, object, obj, templs, renders)
	}

	return renders
}

func (r *Controller) getRendersForMutatingConfig(ctx context.Context, arfields common.ARFields, config v1alpha1.MutationConfig,
	object objInfo, obj map[string]interface{}, templs map[string]v1alpha1.Template, renders []util.RenderItem) []util.RenderItem {
	if !match.IsMustProcess(ctx, arfields, config.Match) {
		logs.Debugf("Check mutation trigger %s match failed", config.Name)
		return renders
	}

	if config.Match.IsEmpty() && !r.checkMatchSelector(ctx, &config, object) {
		logs.Debugf("Check trigger %s checkMatchSelector failed", config.Name)
		return renders
	}

	logs.Debugf("Check trigger %s match selector ok", config.Name)
	if len(config.TemplateRefs) == 0 && object.kind == "Pod" {
		renders = append(renders, r.getRendersForPod(ctx, templs, obj, config)...)
	}

	for _, ref := range config.TemplateRefs {
		if t, ok := templs[ref]; ok {
			renders = append(renders, r.applyTemplate(ctx, t, obj, config.UpdateStrategy)...)
		}
	}
	return renders
}

func (r *Controller) applyTemplate(ctx context.Context, template v1alpha1.Template, obj map[string]interface{}, strategy string) []util.RenderItem {
	var renders []util.RenderItem
	engine := templates.LoadTemplate(ctx, template)
	logs.Debugf("loading tmpl %v, %v", template.Name, template.Spec.Data)
	for _, engineTemplate := range engine.Templates() {
		logs.Debugf("template %v", engineTemplate.Name())
		var tempBuffer strings.Builder
		err := engineTemplate.Execute(&tempBuffer, obj)
		if err != nil {
			logs.Errorf("Controller: getRenders failed %s", err)
			continue
		}
		renders = append(renders, util.RenderItem{Template: template, Render: tempBuffer.String(), Action: strategy})
	}
	return renders
}

// checkMatchSelector Need replaced to universal Match allow function
func (r *Controller) checkMatchSelector(ctx context.Context, config *v1alpha1.MutationConfig, obj objInfo) bool {
	logs.Debugf("checkMatchSelector got annotations %s, labels %s ", obj.annots, obj.labels)
	logs.Debugf("checkMatchSelector ObjectSelector Kind %s, Name %s", obj.kind, obj.name)
	if config.ObjectSelector.Kind != "" && config.ObjectSelector.Name != "" {
		if obj.kind == config.ObjectSelector.Kind {
			matches, err := regexp.Match(config.ObjectSelector.Name, []byte(obj.name))
			if err != nil {
				logs.Errorf("Controller: checkMatchSelector failed %s", err)
			}
			if matches {
				logs.Debugf("checkMatchSelector ObjectSelector Kind %s, Name %s checked ok", obj.kind, obj.name)
				return true
			}
		}
	}

	if obj.labels != nil && len(config.LabelSelector.MatchExpressions) != 0 {
		labelSet := lbs.Set(obj.labels)
		labelSelectors := metav1.LabelSelector{}

		for _, expression := range config.LabelSelector.MatchExpressions {

			labelSelectors.MatchExpressions = append(labelSelectors.MatchExpressions,
				metav1.LabelSelectorRequirement{
					Key:      expression.Key,
					Operator: expression.Operator,
					Values:   expression.Values,
				},
			)

			sel, err := metav1.LabelSelectorAsSelector(&labelSelectors)
			if err != nil {
				logs.Errorf("Controller: checkMatchSelector failed %s", err)
			} else {
				return sel.Matches(labelSet)
			}
		}
	}

	if obj.annots != nil {
		triggerKey := fmt.Sprintf("%s/%s", config.AnnotationNamespace, config.AnnotationTrigger)
		triggerValue, ok := obj.annots[triggerKey]
		if ok {
			logs.Debugf("checkMatchSelector Annotations annotation %s %s ok ", triggerKey, triggerValue)
			if triggerValue != "true" && triggerValue != "enabled" {
				logs.Error("Controller: checkMatchSelector failed skip creation cause annotation value is not valid")
			} else {
				return true
			}
		}
	}

	return false
}

func addMutatingLabel(obj map[string]interface{}) map[string]interface{} {
	if obj == nil {
		return nil
	}
	if _, ok := obj["metadata"]; !ok {
		obj["metadata"] = make(map[string]interface{})
	}

	if _, ok := obj["metadata"].(map[string]interface{})["labels"]; !ok {
		obj["metadata"].(map[string]interface{})["labels"] = make(map[string]interface{})
	}

	obj["metadata"].(map[string]interface{})["labels"].(map[string]interface{})["kblt.mutation.resource"] = "true"
	return obj
}
